All files / remote backoff.ts

100% Statements 33/33
100% Branches 8/8
100% Functions 7/7
100% Lines 32/32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132                                2x     2x                     2x   1255x           1255x       1255x           1255x         1255x           1255x   1255x                   2x 4715x             2x 2x               2x   70x       70x 70x 34x           70x               70x 70x 36x   70x 2x       2x 824x 70x 70x         2x 70x   2x  
/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
import * as log from '../util/log';
import { CancelablePromise } from '../util/promise';
import { AsyncQueue, TimerId } from '../util/async_queue';
const LOG_TAG = 'ExponentialBackoff';
 
/**
 * A helper for running delayed tasks following an exponential backoff curve
 * between attempts.
 *
 * Each delay is made up of a "base" delay which follows the exponential
 * backoff curve, and a +/- 50% "jitter" that is calculated and added to the
 * base delay. This prevents clients from accidentally synchronizing their
 * delays causing spikes of load to the backend.
 */
export class ExponentialBackoff {
  private currentBaseMs: number;
  private timerPromise: CancelablePromise<void> | null = null;
 
  constructor(
    /**
     * The AsyncQueue to run backoff operations on.
     */
    private readonly queue: AsyncQueue,
    /**
     * The ID to use when scheduling backoff operations on the AsyncQueue.
     */
    private readonly timerId: TimerId,
    /**
     * The initial delay (used as the base delay on the first retry attempt).
     * Note that jitter will still be applied, so the actual delay could be as
     * little as 0.5*initialDelayMs.
     */
    private readonly initialDelayMs: number,
    /**
     * The multiplier to use to determine the extended base delay after each
     * attempt.
     */
    private readonly backoffFactor: number,
    /**
     * The maximum base delay after which no further backoff is performed.
     * Note that jitter will still be applied, so the actual delay could be as
     * much as 1.5*maxDelayMs.
     */
    private readonly maxDelayMs: number
  ) {
    this.reset();
  }
 
  /**
   * Resets the backoff delay.
   *
   * The very next backoffAndWait() will have no delay. If it is called again
   * (i.e. due to an error), initialDelayMs (plus jitter) will be used, and
   * subsequent ones will increase according to the backoffFactor.
   */
  reset(): void {
    this.currentBaseMs = 0;
  }
 
  /**
   * Resets the backoff delay to the maximum delay (e.g. for use after a
   * RESOURCE_EXHAUSTED error).
   */
  resetToMax(): void {
    this.currentBaseMs = this.maxDelayMs;
  }
 
  /**
   * Returns a promise that resolves after currentDelayMs, and increases the
   * delay for any subsequent attempts. If there was a pending backoff operation
   * already, it will be canceled.
   */
  backoffAndRun(op: () => Promise<void>): void {
    // Cancel any pending backoff operation.
    this.cancel();
 
    // First schedule using the current base (which may be 0 and should be
    // honored as such).
    const delayWithJitterMs = this.currentBaseMs + this.jitterDelayMs();
    if (this.currentBaseMs > 0) {
      log.debug(
        LOG_TAG,
        `Backing off for ${delayWithJitterMs} ms ` +
          `(base delay: ${this.currentBaseMs} ms)`
      );
    }
    this.timerPromise = this.queue.enqueueAfterDelay(
      this.timerId,
      delayWithJitterMs,
      op
    );
 
    // Apply backoff factor to determine next delay and ensure it is within
    // bounds.
    this.currentBaseMs *= this.backoffFactor;
    if (this.currentBaseMs < this.initialDelayMs) {
      this.currentBaseMs = this.initialDelayMs;
    }
    if (this.currentBaseMs > this.maxDelayMs) {
      this.currentBaseMs = this.maxDelayMs;
    }
  }
 
  cancel(): void {
    if (this.timerPromise !== null) {
      this.timerPromise.cancel();
      this.timerPromise = null;
    }
  }
 
  /** Returns a random value in the range [-currentBaseMs/2, currentBaseMs/2] */
  private jitterDelayMs(): number {
    return (Math.random() - 0.5) * this.currentBaseMs;
  }
}